/*
 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.eclipse.imagen.media.opimage;

import java.awt.Rectangle;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.MultiPixelPackedSampleModel;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.util.Map;
import org.eclipse.imagen.ImageLayout;
import org.eclipse.imagen.PackedImageData;
import org.eclipse.imagen.PixelAccessor;
import org.eclipse.imagen.PointOpImage;
import org.eclipse.imagen.UnpackedImageData;
import org.eclipse.imagen.media.util.ImageUtil;
import org.eclipse.imagen.media.util.JDKWorkarounds;

/**
 * An <code>OpImage</code> implementing the "Binarize" operation as described in <code>
 * org.eclipse.imagen.operator.BinarizeDescriptor</code>.
 *
 * <p>This <code>OpImage</code> maps all the pixels of an image whose value falls within a given range to a constant on
 * a per-band basis. Each of the lower bound, upper bound, and constant arrays may have only one value in it. If that is
 * the case, that value is used for all bands.
 *
 * @see org.eclipse.imagen.operator.BinarizeDescriptor
 * @see BinarizeCRIF
 * @since version 1.1
 */
final class BinarizeOpImage extends PointOpImage {

    /** Lookup table for ORing bytes of output. */
    private static byte[] byteTable = new byte[] {
        (byte) 0x80, (byte) 0x40, (byte) 0x20, (byte) 0x10,
        (byte) 0x08, (byte) 0x04, (byte) 0x02, (byte) 0x01,
    };

    /** bitsOn[j + (i<<3)] sets bits on from i to j */
    private static int[] bitsOn = null;

    /** The threshold. */
    private double threshold;

    /**
     * Constructor.
     *
     * @param source The source image.
     * @param layout The destination image layout.
     * @param threshold The threshold value for binarization.
     */
    public BinarizeOpImage(RenderedImage source, Map config, ImageLayout layout, double threshold) {
        super(source, layoutHelper(source, layout, config), config, true);

        if (source.getSampleModel().getNumBands() != 1) {
            throw new IllegalArgumentException(JaiI18N.getString("BinarizeOpImage0"));
        }

        this.threshold = threshold;
    }

    // set the OpImage's SM to be MultiPixelPackedSampleModel
    private static ImageLayout layoutHelper(RenderedImage source, ImageLayout il, Map config) {

        ImageLayout layout = (il == null) ? new ImageLayout() : (ImageLayout) il.clone();

        SampleModel sm = layout.getSampleModel(source);
        if (!ImageUtil.isBinary(sm)) {
            sm = new MultiPixelPackedSampleModel(
                    DataBuffer.TYPE_BYTE, layout.getTileWidth(source), layout.getTileHeight(source), 1);
            layout.setSampleModel(sm);
        }

        ColorModel cm = layout.getColorModel(null);
        if (cm == null || !JDKWorkarounds.areCompatibleDataModels(sm, cm)) {
            layout.setColorModel(ImageUtil.getCompatibleColorModel(sm, config));
        }

        return layout;
    }

    /**
     * Map the pixels inside a specified rectangle whose value is within a rang to a constant on a per-band basis.
     *
     * @param sources Cobbled sources, guaranteed to provide all the source data necessary for computing the rectangle.
     * @param dest The tile containing the rectangle to be computed.
     * @param destRect The rectangle within the tile to be computed.
     */
    protected void computeRect(Raster[] sources, WritableRaster dest, Rectangle destRect) {
        switch (sources[0].getSampleModel().getDataType()) {
            case DataBuffer.TYPE_BYTE:
                byteLoop(sources[0], dest, destRect);
                break;

            case DataBuffer.TYPE_SHORT:
                shortLoop(sources[0], dest, destRect);
                break;
            case DataBuffer.TYPE_USHORT:
                ushortLoop(sources[0], dest, destRect);
                break;
            case DataBuffer.TYPE_INT:
                intLoop(sources[0], dest, destRect);
                break;

            case DataBuffer.TYPE_FLOAT:
                floatLoop(sources[0], dest, destRect);
                break;
            case DataBuffer.TYPE_DOUBLE:
                doubleLoop(sources[0], dest, destRect);
                break;

            default:
                throw new RuntimeException(JaiI18N.getString("BinarizeOpImage1"));
        }
    }

    private void byteLoop(Raster source, WritableRaster dest, Rectangle destRect) {

        if (threshold <= 0.0D) {
            // every bit is 1
            setTo1(dest, destRect);
            return;
        } else if (threshold > 255.0D) {
            // every bit is zeros;
            return;
        }

        short thresholdI = (short) Math.ceil(threshold);
        // computation can be done in integer
        // even though threshold is of double type
        // int thresholdI = (int)Math.ceil(this.threshold);
        // or through a lookup table for byte case

        Rectangle srcRect = mapDestRect(destRect, 0); // should be identical to destRect

        PixelAccessor pa = new PixelAccessor(dest.getSampleModel(), null);
        PackedImageData pid = pa.getPackedPixels(dest, destRect, true, false);
        int offset = pid.offset;
        PixelAccessor srcPa = new PixelAccessor(source.getSampleModel(), null);

        UnpackedImageData srcImD = srcPa.getPixels(source, srcRect, DataBuffer.TYPE_BYTE, false);
        int srcOffset = srcImD.bandOffsets[0];
        byte[] srcData = ((byte[][]) srcImD.data)[0];
        int pixelStride = srcImD.pixelStride;

        int ind0 = pid.bitOffset;
        for (int h = 0; h < destRect.height; h++) {
            int indE = ind0 + destRect.width;
            for (int b = ind0, s = srcOffset; b < indE; b++, s += pixelStride) {
                if ((srcData[s] & 0xFF) >= thresholdI) {
                    pid.data[offset + (b >> 3)] |= byteTable[b % 8];
                }
            }
            offset += pid.lineStride;
            srcOffset += srcImD.lineStride;
        }
        pa.setPackedPixels(pid);
    }

    // computation in short
    private void shortLoop(Raster source, WritableRaster dest, Rectangle destRect) {

        if (threshold <= Short.MIN_VALUE) {
            // every bit is 1
            setTo1(dest, destRect);
            return;
        } else if (threshold > Short.MAX_VALUE) {
            // every bit is zeros;
            return;
        }

        short thresholdS = (short) (Math.ceil(threshold));
        // computation can be done in integer
        // even though threshold is of double type
        // int thresholdI = (int)Math.ceil(this.threshold);
        // or through a lookup table for byte case

        Rectangle srcRect = mapDestRect(destRect, 0); // should be identical to destRect

        PixelAccessor pa = new PixelAccessor(dest.getSampleModel(), null);
        PackedImageData pid = pa.getPackedPixels(dest, destRect, true, false);
        int offset = pid.offset;
        PixelAccessor srcPa = new PixelAccessor(source.getSampleModel(), null);

        UnpackedImageData srcImD = srcPa.getPixels(source, srcRect, DataBuffer.TYPE_SHORT, false);
        int srcOffset = srcImD.bandOffsets[0];
        short[] srcData = ((short[][]) srcImD.data)[0];
        int pixelStride = srcImD.pixelStride;

        int ind0 = pid.bitOffset;
        for (int h = 0; h < destRect.height; h++) {
            int indE = ind0 + destRect.width;
            for (int b = ind0, s = srcOffset; b < indE; b++, s += pixelStride) {
                if (srcData[s] >= thresholdS) {
                    pid.data[offset + (b >> 3)] |= byteTable[b % 8];
                }
            }
            offset += pid.lineStride;
            srcOffset += srcImD.lineStride;
        }
        pa.setPackedPixels(pid);
    }

    // computation in short
    private void ushortLoop(Raster source, WritableRaster dest, Rectangle destRect) {

        if (threshold <= 0.0D) {
            // every bit is 1
            setTo1(dest, destRect);
            return;
        } else if (threshold > (double) (0xFFFF)) {
            // every bit is zeros;
            return;
        }

        int thresholdI = (int) (Math.ceil(threshold));
        // computation can be done in integer
        // even though threshold is of double type
        // int thresholdI = (int)Math.ceil(this.threshold);
        // or through a lookup table for byte case

        Rectangle srcRect = mapDestRect(destRect, 0); // should be identical to destRect

        PixelAccessor pa = new PixelAccessor(dest.getSampleModel(), null);
        PackedImageData pid = pa.getPackedPixels(dest, destRect, true, false);
        int offset = pid.offset;
        PixelAccessor srcPa = new PixelAccessor(source.getSampleModel(), null);

        UnpackedImageData srcImD = srcPa.getPixels(source, srcRect, DataBuffer.TYPE_USHORT, false);
        int srcOffset = srcImD.bandOffsets[0];
        short[] srcData = ((short[][]) srcImD.data)[0];
        int pixelStride = srcImD.pixelStride;

        int ind0 = pid.bitOffset;
        for (int h = 0; h < destRect.height; h++) {
            int indE = ind0 + destRect.width;
            for (int b = ind0, s = srcOffset; b < indE; b++, s += pixelStride) {
                if ((srcData[s] & 0xFFFF) >= thresholdI) {
                    pid.data[offset + (b >> 3)] |= byteTable[b % 8];
                }
            }
            offset += pid.lineStride;
            srcOffset += srcImD.lineStride;
        }
        pa.setPackedPixels(pid);
    }

    private void intLoop(Raster source, WritableRaster dest, Rectangle destRect) {

        if (threshold <= Integer.MIN_VALUE) {
            // every bit is 1
            setTo1(dest, destRect);
            return;
        } else if (threshold > (double) Integer.MAX_VALUE) {
            // every bit is zeros;
            return;
        }

        // computation can be done in integer
        // even though threshold is of double type
        int thresholdI = (int) Math.ceil(this.threshold);

        // computation can be done in integer
        // even though threshold is of double type
        // int thresholdI = (int)Math.ceil(this.threshold);

        Rectangle srcRect = mapDestRect(destRect, 0); // should be identical to destRect

        PixelAccessor pa = new PixelAccessor(dest.getSampleModel(), null);
        PackedImageData pid = pa.getPackedPixels(dest, destRect, true, false);
        int offset = pid.offset;
        PixelAccessor srcPa = new PixelAccessor(source.getSampleModel(), null);

        UnpackedImageData srcImD = srcPa.getPixels(source, srcRect, DataBuffer.TYPE_INT, false);
        int srcOffset = srcImD.bandOffsets[0];
        int[] srcData = ((int[][]) srcImD.data)[0];
        int pixelStride = srcImD.pixelStride;

        int ind0 = pid.bitOffset;
        for (int h = 0; h < destRect.height; h++) {
            int indE = ind0 + destRect.width;
            for (int b = ind0, s = srcOffset; b < indE; b++, s += pixelStride) {
                if (srcData[s] >= threshold) {
                    pid.data[offset + (b >> 3)] |= byteTable[b % 8];
                }
            }
            offset += pid.lineStride;
            srcOffset += srcImD.lineStride;
        }
        pa.setPackedPixels(pid);
    }

    // computation in float
    private void floatLoop(Raster source, WritableRaster dest, Rectangle destRect) {

        Rectangle srcRect = mapDestRect(destRect, 0); // should be identical to destRect

        PixelAccessor pa = new PixelAccessor(dest.getSampleModel(), null);
        PackedImageData pid = pa.getPackedPixels(dest, destRect, true, false);
        int offset = pid.offset;
        PixelAccessor srcPa = new PixelAccessor(source.getSampleModel(), null);

        UnpackedImageData srcImD = srcPa.getPixels(source, srcRect, DataBuffer.TYPE_FLOAT, false);
        int srcOffset = srcImD.bandOffsets[0];
        float[] srcData = ((float[][]) srcImD.data)[0];
        int pixelStride = srcImD.pixelStride;

        int ind0 = pid.bitOffset;
        for (int h = 0; h < destRect.height; h++) {
            int indE = ind0 + destRect.width;
            for (int b = ind0, s = srcOffset; b < indE; b++, s += pixelStride) {
                if (srcData[s] > threshold) {
                    pid.data[offset + (b >> 3)] |= byteTable[b % 8];
                }
            }
            offset += pid.lineStride;
            srcOffset += srcImD.lineStride;
        }
        pa.setPackedPixels(pid);
    }

    // computation in double
    private void doubleLoop(Raster source, WritableRaster dest, Rectangle destRect) {

        Rectangle srcRect = mapDestRect(destRect, 0); // should be identical to destRect

        PixelAccessor pa = new PixelAccessor(dest.getSampleModel(), null);
        PackedImageData pid = pa.getPackedPixels(dest, destRect, true, false);
        int offset = pid.offset;
        PixelAccessor srcPa = new PixelAccessor(source.getSampleModel(), null);

        UnpackedImageData srcImD = srcPa.getPixels(source, srcRect, DataBuffer.TYPE_DOUBLE, false);
        int srcOffset = srcImD.bandOffsets[0];
        double[] srcData = ((double[][]) srcImD.data)[0];
        int pixelStride = srcImD.pixelStride;

        int ind0 = pid.bitOffset;
        for (int h = 0; h < destRect.height; h++) {
            int indE = ind0 + destRect.width;
            for (int b = ind0, s = srcOffset; b < indE; b++, s += pixelStride) {
                if (srcData[s] > threshold) {
                    pid.data[offset + (b >> 3)] |= byteTable[b % 8];
                }
            }
            offset += pid.lineStride;
            srcOffset += srcImD.lineStride;
        }
        pa.setPackedPixels(pid);
    }

    // set all bits in a rectangular region to be 1
    // need to be sure that paddings not changing
    private void setTo1(Raster dest, Rectangle destRect) {
        initBitsOn();
        PixelAccessor pa = new PixelAccessor(dest.getSampleModel(), null);
        PackedImageData pid = pa.getPackedPixels(dest, destRect, true, false);
        int offset = pid.offset;

        for (int h = 0; h < destRect.height; h++) {
            int ind0 = pid.bitOffset;
            int indE = ind0 + destRect.width - 1;
            if (indE < 8) {
                // the entire row in data[offset]
                pid.data[offset] = (byte) (pid.data[offset] | bitsOn[indE]); // (0<<3) + indE
            } else {
                // 1st byte
                pid.data[offset] = (byte) (pid.data[offset] | bitsOn[7]); // (0<<3) + 7
                // middle bytes
                for (int b = offset + 1; b <= offset + (indE - 7) / 8; b++) {
                    pid.data[b] = (byte) (0xff);
                }
                // last byte

                int remBits = indE % 8;
                if (remBits % 8 != 7) {
                    indE = offset + indE / 8;
                    pid.data[indE] = (byte) (pid.data[indE] | bitsOn[remBits]); // (0<<3)+remBits
                }
            }
            offset += pid.lineStride;
        }
        pa.setPackedPixels(pid);
    }

    // setting bits i to j to 1;
    //  i <= j
    private static synchronized void initBitsOn() {

        if (bitsOn != null) return;

        bitsOn = new int[64];
        for (int i = 0; i < 8; i++) {
            for (int j = i; j < 8; j++) {
                int bi = (0x00ff) >> i;
                int bj = (0x00ff) << (7 - j);
                bitsOn[j + (i << 3)] = bi & bj;
            }
        }
    }
}
